Dua app Android (user + admin) + backend API untuk cek nominal pajak kendaraan: user kirim plat + no WA, admin input 4 nominal, user transfer & konfirmasi via WhatsApp.
User login Google (Credential Manager) -> input plat (prefix KB hardcoded) + nomor WhatsApp -> backend simpan request PENDING + push FCM ke admin. Admin app: antrian pending -> detail -> input 4 nominal (pajak, biaya cek, biaya admin, biaya kirim) -> kirim; total dihitung backend. User dapat push -> layar rincian + total -> tombol Bayar -> layar rekening (multi-bank, salin no rek) + tombol konfirmasi WhatsApp (wa.me prefilled) + upload bukti transfer -> admin verifikasi -> status PAID/STRUK_SENT + push. Desain: variant V1 Clean Minimal (putih + biru #0a7cff, Linear-style).
Sistem cek pajak kendaraan (region Bantul, prefix plat KB) terdiri dari 3 bagian:
1. pajak-kb-api — backend Next.js (App Router) + Prisma + PostgreSQL + Firebase Cloud Messaging, deploy di Coolify.
2. pajak-kb-user — app Android (Kotlin) untuk pemilik kendaraan.
3. pajak-kb-admin — app Android (Kotlin) untuk admin yang mengisi nominal.
Alur inti: user kirim plat + no WA -> admin input 4 nominal -> user transfer -> upload bukti + konfirmasi WhatsApp -> admin verifikasi.
Login
User app pakai Credential Manager (Google Sign-In) -> dapat ID token -> POST /api/auth/google { idToken, fcmToken } -> backend verifikasi via google-auth-library (audience = web client ID) -> upsert User -> balikan JWT HS256 (30 hari) + role.
Role ditentukan whitelist env ADMIN_EMAILS — dievaluasi ulang setiap login, bukan sign-up bebas.
Request pajak
User app: input plat (regex ^KB \d{1,4} [A-Z]{1,3}$, prefix KB fixed di UI) + nomor WhatsApp -> POST /api/tax-request { platNomor, whatsapp }.
Backend: normalisasi plat (KB1475WQ -> KB 1475 WQ) dan WA (08xx -> 62xx), rate limit 5 request/jam per user, simpan status PENDING, kirim FCM ke semua admin.
Respon admin
Admin app: GET /api/tax-request?scope=all&status=PENDING -> buka detail -> POST /api/tax-request/{id}/response { nominalPajak, biayaCek, biayaAdmin, biayaKirim }.
Backend: hanya role ADMIN, hanya kalau status PENDING, simpan 4 nominal + respondedAt + responseExpires (24 jam) + buat record Payment status WAITING_UPLOAD -> push FCM ke user.
Pembayaran
User app: GET /api/bank-accounts (multi-bank: BCA, Mandiri) -> tampil no rekening + tombol salin -> GET /api/payment/{id}/confirm-link balikan URL wa.me dengan pesan prefilled (plat, request id, total).
User upload bukti: POST /api/payment/{id}/upload (multipart, max 8MB, jpeg/png/webp/pdf) -> Payment jadi WAITING_VERIFY -> push ke admin.
Admin verifikasi: POST /api/payment/{id}/verify { sendStruk?: true } -> PAID atau STRUK_SENT -> push ke user.
model User {
id, googleSub @unique, email @unique, name?, role (USER|ADMIN),
fcmToken?, whatsapp?, createdAt
}
model TaxRequest {
id, platNomor, whatsapp, status (PENDING|RESPONDED|CANCELLED|EXPIRED),
nominalPajak?, biayaCek?, biayaAdmin?, biayaKirim?,
userId, adminId?, payment?,
createdAt, respondedAt?, responseExpires?, updatedAt
}
model Payment {
id, requestId @unique, status (WAITING_UPLOAD|WAITING_VERIFY|PAID|STRUK_SENT),
proofUrl?, verifiedAt?, verifiedBy?
}
model BankAccount {
id, bank, number, holder, active, order
}model User {
id, googleSub @unique, email @unique, name?, role (USER|ADMIN),
fcmToken?, whatsapp?, createdAt
}
model TaxRequest {
id, platNomor, whatsapp, status (PENDING|RESPONDED|CANCELLED|EXPIRED),
nominalPajak?, biayaCek?, biayaAdmin?, biayaKirim?,
userId, adminId?, payment?,
createdAt, respondedAt?, responseExpires?, updatedAt
}
model Payment {
id, requestId @unique, status (WAITING_UPLOAD|WAITING_VERIFY|PAID|STRUK_SENT),
proofUrl?, verifiedAt?, verifiedBy?
}
model BankAccount {
id, bank, number, holder, active, order
}
Total tagihan = nominalPajak + biayaCek + biayaAdmin + biayaKirim (dihitung backend, bukan dikirim klien).
Total tagihan = nominalPajak + biayaCek + biayaAdmin + biayaKirim (dihitung backend, bukan dikirim klien).
| Method | Path | Akses | Fungsi |
|---|---|---|---|
| POST | /api/auth/google | publik | Verifikasi Google ID token, upsert user, balikan JWT |
| GET | /api/me | auth | Profil user |
| PATCH | /api/me | auth | Update whatsapp / fcmToken |
| POST | /api/tax-request | auth | Buat request (plat + WA) |
| GET | /api/tax-request | auth | List request (?status=&scope=all khusus admin) |
| GET | /api/tax-request/{id} | auth | Detail (user: milik sendiri) |
| POST | /api/tax-request/{id}/response | admin | Input 4 nominal |
| POST | /api/tax-request/{id}/cancel | auth | Batalkan (hanya PENDING) |
| GET | /api/bank-accounts | auth | Daftar rekening aktif |
| GET | /api/payment/{id}/confirm-link | auth | URL wa.me prefilled |
| POST | /api/payment/{id}/upload | auth | Upload bukti transfer (multipart) |
| POST | /api/payment/{id}/verify | admin | Verifikasi pembayaran |
| GET | /api/uploads/{file} | publik | Serve file bukti (blok path traversal) |
| GET | /api/health | publik | Healthcheck Coolify |
DATABASE_URL=postgres://postgres:<PASS>@<container>:5432/postgres?schema=public JWT_SECRET=<32+ char random> GOOGLE_CLIENT_ID=<web client id> ADMIN_EMAILS=andy@example.com,admin2@example.com ADMIN_WA_NUMBER=62812xxxxxxx FCM_SERVICE_ACCOUNT_B64=<base64 service account json> APP_BASE_URL=https://... UPLOAD_DIR=data/uploads
DATABASE_URL=postgres://postgres:<PASS>@<container>:5432/postgres?schema=public
JWT_SECRET=<32+ char random>
GOOGLE_CLIENT_ID=<web client id>
ADMIN_EMAILS=andy@example.com,admin2@example.com
ADMIN_WA_NUMBER=62812xxxxxxx
FCM_SERVICE_ACCOUNT_B64=<base64 service account json>
APP_BASE_URL=https://...
UPLOAD_DIR=data/uploads
Setup produksi: Coolify -> New resource dari GitHub `djvpri/pajak-kb-api` -> build pack Nixpacks -> tambah Postgres -> set env -> deploy -> jalankan `npm run db:migrate && npm run db:seed`. Catatan: prisma harus v5 (`prisma@5` + `@prisma/client@5`); prisma 8 rc tidak punya command `generate` di CLI-nya.
Setup produksi: Coolify -> New resource dari GitHub djvpri/pajak-kb-api -> build pack Nixpacks -> tambah Postgres -> set env -> deploy -> jalankan npm run db:migrate && npm run db:seed.
Catatan: prisma harus v5 (prisma@5 + @prisma/client@5); prisma 8 rc tidak punya command generate di CLI-nya.
Request
PENDING -> (admin isi nominal) -> RESPONDED
PENDING -> (user/admin batal) -> CANCELLED
RESPONDED -> (lewat responseExpires 24 jam) -> EXPIRED
Payment
WAITING_UPLOAD -> (user upload bukti) -> WAITING_VERIFY
WAITING_VERIFY -> (admin verify) -> PAID
PAID -> (admin kirim struk) -> STRUK_SENT
Dipilih dari 3 variant mockup. Karakter: putih + biru #0a7cff, whitespace lebar, tipografi system font, kartu border tipis #e3e6ea, radius 10–12px.
User app — 5 layar
1. Login — judul "Cek Pajak Kendaraan" + tombol "Lanjutkan dengan Google".
2. Input — plat 3 kotak (KB fixed | angka | kode), field nomor WhatsApp dengan prefix +62 hijau, tombol "Kirim Permintaan".
3. Menunggu — tag status, plat, spinner, request ID, tombol Batalkan.
4. Rincian — total besar biru, 4 baris (pajak, biaya cek, biaya admin, biaya kirim), tombol Bayar.
5. Pembayaran — total, kartu rekening (bank, no rek + tombol Salin, atas nama), tombol hijau "Konfirmasi via WhatsApp", catatan upload bukti.
Admin app — 4 layar
1. Antrian — tab Pending/Selesai/Batal dengan badge, kartu request (plat, nama, no WA, waktu).
2. Detail — plat highlight, nama, no WA, waktu, tombol Input Nominal / Tolak.
3. Input Nominal — 4 field rupiah + total auto-sum, tombol Kirim ke User.
4. Konfirmasi — checkmark, status Selesai, rincian + total, tombol Lihat Antrian.
1. Backend: push kode (selesai, 37 file di djvpri/pajak-kb-api), set env Coolify, jalankan migration + seed, deploy.
2. Google Cloud: buat OAuth client Android untuk com.pajakkb.user dan com.pajakkb.admin (package name + SHA-1), web client untuk verifikasi token backend.
3. Firebase: buat project / reuse, download service account JSON -> base64 -> FCM_SERVICE_ACCOUNT_B64; google-services.json untuk kedua app.
4. User app: Kotlin + Compose, Credential Manager, Retrofit, FCM, 5 layar sesuai desain.
5. Admin app: Kotlin + Compose, role guard (hanya ADMIN boleh masuk), 4 layar.
6. Belum dibuat: cron expire request lewat 24 jam (scripts/expire.mjs), rate limit pindah ke Redis kalau multi-instance, storage bukti pindah S3/R2 kalau traffic naik.